Adding Offline Storage, Push Notifications, and Performance Optimization in React Native
Course: React Native Training
Topic: Adding Offline Storage, Push Notifications, and Performance Optimization
Course Link: React Native Training – JustAcademy
Demo Link: Register for a Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction
Modern mobile applications should remain useful even when the internet connection is slow, temporarily unavailable, or unreliable. They should also keep users informed about important activities and respond smoothly to user interactions. In React Native, these goals can be achieved by implementing offline storage, push notifications, and performance optimization.
Offline storage allows an application to save selected information on the device so it can be accessed after a restart or while the user is offline. Push notifications deliver messages from a server to a device, even when the application is not actively open, subject to operating system restrictions and user permissions. Performance optimization improves loading time, scrolling, animations, memory usage, and general responsiveness.
1.1 Why These Features Are Important
- Better user experience: Users can access previously saved information without waiting for every request to reach the server.
- Offline access: Selected information can remain available when connectivity is lost.
- Reliable interactions: Applications can queue supported actions and synchronize them when a connection returns.
- User engagement: Notifications can communicate messages, reminders, order updates, or other relevant events.
- Faster screens: Caching, efficient lists, and controlled rendering can reduce unnecessary work.
- Lower resource usage: Appropriate caching and efficient processing can reduce network activity, memory pressure, and battery consumption.
1.2 Learning Objectives
- Understand offline-first application design.
- Store and retrieve data using AsyncStorage.
- Understand when to use SQLite and secure storage.
- Implement basic offline caching and data synchronization.
- Understand the difference between local and remote push notifications.
- Request notification permissions and register a device token.
- Handle notifications when an application is active or opened from a notification.
- Optimize React Native lists, components, images, and API requests.
- Measure application performance and investigate common bottlenecks.
- Build a practical application that combines all three areas.
2. Understanding Offline Storage
Offline storage means saving data on the device so an application can retrieve it without making a new network request every time. It is especially useful for settings, cached product information, saved drafts, recently viewed content, and other information that should remain accessible between sessions.
2.1 How Offline Storage Works
- The application requests data from a server.
- The server returns the requested data.
- The application displays the data to the user.
- The application saves an appropriate copy of the data locally.
- When the same information is required again, the application can read the local copy first.
- When connectivity returns, the application can request newer server data and update the local copy.
Offline storage is not the same as offline synchronization. Storage keeps data on the device, while synchronization coordinates local and remote changes so that the two versions remain consistent.
2.2 Examples of Data Suitable for Offline Access
- Previously viewed product details.
- Cached article or course content.
- Application preferences and theme settings.
- Shopping cart contents, where the business rules allow it.
- Saved drafts and unfinished forms.
- Previously loaded order summaries.
- Reference data such as categories and lookup values.
- Queued user actions that can safely be synchronized later.
2.3 Information That Requires Special Care
Not all data should be saved in the same way. Passwords, authentication tokens, payment credentials, personal information, and confidential business records may require secure storage, encryption, access controls, limited retention, or no local persistence at all. The application should store only the information required to support its features.
3. Types of Offline Storage in React Native
| Storage Type |
Suitable For |
Important Consideration |
| AsyncStorage |
Simple key-value data, preferences, small caches |
It is unencrypted and stores string values. |
| SQLite |
Structured records, searchable offline data, relational information |
Requires database schema and query management. |
| Secure storage |
Small sensitive values such as authentication tokens |
Use an appropriate platform-backed secure storage solution. |
| In-memory state |
Temporary UI state and currently displayed information |
Data normally disappears when the application process is terminated. |
| File storage |
Downloaded documents, images, and larger files |
Requires file lifecycle and storage-space management. |
| Application cache |
Data that can be recreated from a server |
Cached content can be cleared or expire. |
3.1 Choosing the Right Storage Type
Use a simple key-value store for small preferences and compact serialized data. Use SQLite when the application needs queries, filtering, relationships, or structured collections of records. Use platform-backed secure storage for suitable secrets. Use file storage when working with documents or large downloads. Select a solution based on data size, sensitivity, querying needs, and synchronization requirements.
4. Using AsyncStorage in React Native
AsyncStorage is a community-maintained persistent key-value storage library for React Native. It provides asynchronous operations for saving, retrieving, and removing values. It is useful when an application needs to remember non-sensitive information across launches.
4.1 Install AsyncStorage
npm install @react-native-async-storage/async-storage
For projects using Expo, install the package using the compatible Expo installation command when appropriate. Follow the current package documentation for the version installed in your project. Some versions expose a storage factory, while older examples use a default AsyncStorage export.
4.2 Create a Storage Service
Keeping storage operations in a service module avoids duplicating storage logic throughout the application.
// storageService.js
import { createAsyncStorage } from
"@react-native-async-storage/async-storage";
const storage = createAsyncStorage("appDB");
export async function saveData(key, value) {
try {
await storage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error("Unable to save local data.", error);
return false;
}
}
export async function getData(key) {
try {
const value = await storage.getItem(key);
if (value === null) {
return null;
}
return JSON.parse(value);
} catch (error) {
console.error("Unable to read local data.", error);
return null;
}
}
export async function removeData(key) {
try {
await storage.removeItem(key);
return true;
} catch (error) {
console.error("Unable to remove local data.", error);
return false;
}
}
The example uses the storage factory pattern available in current AsyncStorage documentation. If the installed version uses a different export, adapt the import and initialization to that version. The helper functions return simple results for demonstration; a production service may use structured errors so screens can distinguish missing data from storage failures.
4.3 Saving Data
import { saveData } from "./storageService";
const userPreferences = {
theme: "dark",
language: "en",
notificationsEnabled: true
};
async function savePreferences() {
const saved = await saveData(
"user_preferences",
userPreferences
);
if (saved) {
console.log("Preferences saved.");
}
}
JSON.stringify converts an object into a string that can be stored. Without serialization, objects and arrays cannot be saved as ordinary key-value string values.
4.4 Reading Saved Data
import { getData } from "./storageService";
async function loadPreferences() {
const preferences = await getData("user_preferences");
if (preferences) {
console.log(preferences.theme);
console.log(preferences.language);
} else {
console.log("No saved preferences found.");
}
}
4.5 Removing Saved Data
import { removeData } from "./storageService";
async function clearPreferences() {
await removeData("user_preferences");
}
Use clear deletion policies. For example, when a customer logs out, remove or isolate that customer's cached private data so that another user on the same device cannot see it.
5. Persisting Application State
Many React Native applications keep their current state in React hooks or a global state manager. This state is useful during the current session, but it does not automatically survive application restarts. Persisting selected state makes it possible to restore useful information when the app opens again.
5.1 Example: Saving a Shopping Cart
const CART_KEY = "shopping_cart";
async function persistCart(cartItems) {
await saveData(CART_KEY, cartItems);
}
async function restoreCart() {
const items = await getData(CART_KEY);
return Array.isArray(items) ? items : [];
}
This pattern can be used for cart contents, saved drafts, or other serializable state. Always validate stored values before using them because the stored content may be incomplete, outdated, or from an older application version.
5.2 Load State Before Saving State
A common mistake is initializing state as an empty array and immediately saving that array before loading the previously saved value. This can overwrite valid cached data.
const [cartItems, setCartItems] = useState([]);
const [cartLoaded, setCartLoaded] = useState(false);
useEffect(() => {
let active = true;
restoreCart()
.then((items) => {
if (active) {
setCartItems(items);
}
})
.finally(() => {
if (active) {
setCartLoaded(true);
}
});
return () => {
active = false;
};
}, []);
useEffect(() => {
if (cartLoaded) {
persistCart(cartItems);
}
}, [cartItems, cartLoaded]);
This is a conceptual integration snippet. Place it inside the appropriate component or state provider and ensure the storage functions are imported. For more demanding persistence requirements, consider serialized writes, versioned data, and a dedicated state persistence library.
6. Using SQLite for Structured Offline Data
SQLite is a local relational database. It is useful when an application needs to store many records, filter data, execute queries, maintain relationships, or update structured data reliably. In Expo applications, the expo-sqlite package provides access to a persistent SQLite database.
6.1 Install SQLite
npx expo install expo-sqlite
For a non-Expo React Native project, verify the package's current installation requirements and ensure the Expo modules infrastructure is configured if the chosen SQLite library requires it.
6.2 Create a Database and Table
import * as SQLite from "expo-sqlite";
const dbPromise = SQLite.openDatabaseAsync("offline.db");
export async function initializeDatabase() {
const db = await dbPromise;
await db.execAsync(`
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
price REAL NOT NULL,
updatedAt TEXT
);
`);
return db;
}
The table stores a product identifier, name, price, and optional update timestamp. A real application may add indexes, category fields, availability information, schema migrations, and constraints that match its data model.
6.3 Insert or Update a Product
export async function saveProduct(product) {
const db = await initializeDatabase();
await db.runAsync(
`INSERT INTO products (id, name, price, updatedAt)
VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
price = excluded.price,
updatedAt = excluded.updatedAt`,
product.id,
product.name,
product.price,
product.updatedAt ?? null
);
}
Parameterized SQL statements help keep user-controlled values separate from query syntax. Avoid inserting untrusted values into SQL strings. The exact SQL features available depend on the SQLite version included with the application.
6.4 Retrieve Offline Products
export async function getOfflineProducts() {
const db = await initializeDatabase();
return db.getAllAsync(
"SELECT id, name, price, updatedAt FROM products ORDER BY name"
);
}
6.5 Delete a Record
export async function deleteProduct(productId) {
const db = await initializeDatabase();
await db.runAsync(
"DELETE FROM products WHERE id = ?",
productId
);
}
6.6 AsyncStorage Compared with SQLite
| Requirement |
Suitable Starting Point |
| Save a theme preference |
AsyncStorage |
| Store a small settings object |
AsyncStorage |
| Search thousands of cached records |
SQLite or another structured database |
| Filter offline product data by multiple fields |
SQLite |
| Store a large collection of related records |
SQLite |
| Store a small sensitive credential |
Platform-backed secure storage |
7. Secure Storage and Data Protection
Persistent storage makes data available after an application restart, but it also creates security responsibilities. AsyncStorage is not encrypted and should not be treated as a secure vault for passwords, private API keys, or sensitive credentials.
7.1 What Should Not Be Stored in Ordinary AsyncStorage?
- Raw passwords.
- Private server API keys.
- Payment card security codes.
- Long-lived secrets that require secure storage.
- Sensitive personal information without a justified need and appropriate protections.
7.2 Use Secure Storage When Required
For Expo applications, expo-secure-store can store small key-value items using platform-supported secure mechanisms. Bare React Native applications can use an appropriate maintained keychain or secure-storage library.
npx expo install expo-secure-store
import * as SecureStore from "expo-secure-store";
export async function saveSessionToken(token) {
await SecureStore.setItemAsync("session_token", token);
}
export async function getSessionToken() {
return await SecureStore.getItemAsync("session_token");
}
export async function removeSessionToken() {
await SecureStore.deleteItemAsync("session_token");
}
This example demonstrates API usage. Handle failures, session renewal, expiration, and logout properly. Secure storage reduces certain risks but does not remove the need for server-side authentication and authorization.
7.3 Offline Data Security Best Practices
- Store only what the feature actually needs.
- Use secure storage for appropriate secrets.
- Clear user-specific cached data on logout when required.
- Validate data retrieved from local storage.
- Keep private API keys and authoritative business rules on the backend.
- Define how long cached information may remain on the device.
- Consider encryption and database protection for sensitive offline datasets.
- Avoid including personal information and tokens in diagnostic logs.
8. Designing an Offline-First Application
An offline-first application treats local data as an important part of the user experience rather than assuming every screen can always reach the server. The app can display saved data immediately and refresh it when connectivity is available.
8.1 Basic Offline-First Workflow
- Open the screen and read the most recent usable local data.
- Display cached content when available.
- Attempt to retrieve current data from the server.
- Validate and save a successful response locally.
- Update the interface with the refreshed information.
- If the request fails, continue showing usable cached content and explain that it may be outdated.
8.2 Example: Cache-First Product Loading
async function loadProductsOfflineFirst() {
// Show cached data as soon as it is available.
const cachedProducts = await getOfflineProducts();
setProducts(cachedProducts);
try {
const response = await fetch(
"https://api.example.com/products"
);
if (!response.ok) {
throw new Error("Product request failed.");
}
const latestProducts = await response.json();
for (const product of latestProducts) {
await saveProduct(product);
}
setProducts(latestProducts);
setOfflineMessage("");
} catch (error) {
setOfflineMessage(
cachedProducts.length > 0
? "Showing saved products. Refresh when connected."
: "No connection and no saved products are available."
);
}
}
This example illustrates the cache-first idea. The helper functions must be imported, state setters must be defined by the screen, and actual responses must be validated. For a large product list, batch database writes inside a transaction rather than issuing many individual writes. Also consider removing stale records when products are deleted from the server.
8.3 Communicating Offline Status
Users should understand when the information they see was last refreshed or when an action cannot currently reach the server.
- Display a connection-status message when appropriate.
- Show a last-updated time for data that can become stale.
- Differentiate cached information from live server results.
- Provide retry actions for recoverable failures.
- Preserve unfinished drafts when possible.
- Do not promise that an action has completed on the server merely because it has been saved locally.
9. Synchronizing Offline Changes
Synchronization moves selected changes between local storage and the server. Reading cached information is generally simpler than synchronizing edits because the application must handle conflicting changes, failed requests, retries, and duplicate submissions.
9.1 Example of Offline Actions
A task-management application could allow a user to create a task while offline. The task is saved locally and marked as pending. When the device reconnects, the application sends the task to the server and marks it synchronized after receiving confirmation.
9.2 Track Synchronization State
| State |
Meaning |
Typical Action |
| Pending |
The change is saved locally but not confirmed by the server. |
Keep it in the sync queue. |
| Syncing |
A request is currently being sent. |
Prevent uncontrolled duplicate processing. |
| Synced |
The server has confirmed the change. |
Record confirmation and update local state. |
| Failed |
The attempt failed or requires user attention. |
Retry safely or show an explanation. |
| Conflict |
The local and remote versions cannot be merged automatically. |
Apply a documented conflict-resolution rule. |
9.3 Basic Retry Pattern
async function syncPendingActions(pendingActions) {
for (const action of pendingActions) {
try {
await sendActionToServer(action);
await markActionSynced(action.id);
} catch (error) {
await recordSyncFailure(action.id, error.message);
}
}
}
This is a conceptual pattern. Functions such as sendActionToServer, markActionSynced, and recordSyncFailure must be implemented. A production sync queue should include unique operation IDs, retry limits, exponential backoff where suitable, authentication handling, and safe duplicate-request protection.
9.4 Handling Synchronization Conflicts
A conflict occurs when the same record has changed locally and remotely. Common strategies include:
- Server wins: Replace the local version with the server version.
- Latest version wins: Choose a version based on trustworthy version metadata or timestamps. Device clocks alone may be unreliable.
- Field-level merge: Combine non-conflicting changes.
- User resolution: Ask the user to choose when changes cannot be merged safely.
Select a strategy based on the data. For financial records, inventory, permissions, and other critical information, the server must enforce the final business rules.
10. Monitoring Network Connectivity
Connectivity information can help determine whether to attempt a network request or show an offline message. However, a device reporting an active connection does not guarantee that a particular server is reachable.
10.1 Using a Connectivity Library
React Native applications commonly use a maintained connectivity library such as @react-native-community/netinfo.
npm install @react-native-community/netinfo
import NetInfo from "@react-native-community/netinfo";
export async function checkConnectivity() {
const state = await NetInfo.fetch();
return {
connected: Boolean(state.isConnected),
internetReachable: state.isInternetReachable
};
}
Interpret unknown connectivity values carefully. Use the actual network request result to determine whether a specific API operation succeeded. Configure any native dependencies according to the project's setup requirements.
10.2 Connectivity Best Practices
- Avoid repeatedly polling the network without a clear need.
- Refresh stale data when a useful connection becomes available.
- Keep offline screens usable where possible.
- Use timeouts and sensible retry policies.
- Do not retry permanent validation or authorization failures indefinitely.
- Prevent multiple connectivity events from launching duplicate synchronization jobs.
11. Introduction to Push Notifications
Push notifications are messages delivered to a device through a notification service. They can inform users about new messages, order updates, reminders, account events, or other relevant information.
A local notification is scheduled by the application on the device. A push notification is generally triggered by a remote system and delivered through a platform push service. The application may need to obtain user permission and configure the appropriate credentials before remote notifications work.
11.1 Common Push Notification Use Cases
- Order confirmation and shipping updates.
- Chat and messaging alerts.
- Appointment reminders.
- Course announcements and learning reminders.
- Important account and security alerts.
- Task deadlines and workflow notifications.
- Time-sensitive updates that a user has chosen to receive.
11.2 Local Notifications Versus Push Notifications
| Feature |
Local Notification |
Push Notification |
| Initiated by |
The app on the device |
A remote server or service |
| Network required to schedule |
Usually not required |
Usually required to send and deliver remotely |
| Typical example |
A reminder scheduled for later |
A server sends an order status update |
| Device token required |
Not typically required |
Usually required by the chosen push service |
| Delivery guarantee |
Subject to operating-system scheduling behavior |
Subject to permissions, network, service behavior, and device restrictions |
12. Understanding Push Notification Architecture
Push notifications involve more than a single mobile application. A typical architecture includes a React Native client, a backend server, a push service, and the operating system on the target device.
12.1 Main Components
- React Native application: Requests permission, registers for notifications, receives notification events, and routes users to relevant screens.
- Application backend: Decides which user should receive a notification and sends the notification request.
- Push service: Delivers the message toward the target device. Common choices include Expo Push Service, Firebase Cloud Messaging (FCM), and Apple Push Notification service (APNs).
- Operating system: Applies platform permission, presentation, and delivery rules.
- User: Grants or denies permission and interacts with the notification.
12.2 Basic Push Notification Flow
- The application requests notification permission when appropriate.
- The application obtains a push token from the selected notification service.
- The application registers the token with its backend and associates it with the appropriate authenticated user or installation.
- An event occurs on the backend, such as an order being shipped.
- The backend creates a notification message and sends it to the configured push service.
- The push service attempts to deliver the message to the target device.
- The operating system displays the notification according to its rules and user settings.
- If the user taps the notification, the application handles the response and navigates to the relevant content.
13. Setting Up Push Notifications with Expo
Expo provides the expo-notifications library for obtaining tokens, handling incoming notifications, interacting with notification responses, and scheduling local notifications. Expo Push Service can simplify sending remote notifications, while direct FCM or APNs integration provides an alternative for projects that need a different delivery architecture.
13.1 Install Required Packages
npx expo install expo-notifications expo-constants
Follow the current Expo push notification setup guide for project configuration, credentials, Android channels, and build requirements. Push notifications require the appropriate native configuration. Remote push notification testing should use a supported physical device or an appropriately configured emulator or simulator. Do not assume that every development runtime supports the complete production notification workflow.
13.2 Configure the Expo Project
Set up the Expo project and notification credentials according to the current push notification setup documentation. For token registration, obtain the correct EAS project ID from the application configuration. Platform requirements can change, so verify the setup steps for the version of Expo and the native operating systems being targeted.
13.3 Import Notification Modules
import React, { useEffect, useState } from "react";
import { Platform } from "react-native";
import * as Notifications from "expo-notifications";
import Constants from "expo-constants";
14. Requesting Notification Permission
Mobile operating systems control whether applications may present notifications. The application should explain the value of notifications and ask for permission at an appropriate point, rather than requesting permission without context.
14.1 Check and Request Permission
async function requestNotificationPermission() {
const existingPermission =
await Notifications.getPermissionsAsync();
let finalStatus = existingPermission.status;
if (finalStatus !== "granted") {
const requestedPermission =
await Notifications.requestPermissionsAsync();
finalStatus = requestedPermission.status;
}
return finalStatus === "granted";
}
This helper returns whether the reported permission status is granted. If permission is denied or restricted, handle that result gracefully and explain how users can change notification settings if they choose. Permission behavior and available options vary by platform version.
14.2 Permission Best Practices
- Explain why notifications are useful before requesting permission.
- Request permission when the user reaches a feature that benefits from notifications.
- Continue to support the application when permission is denied.
- Respect notification settings and topic preferences.
- Avoid repeatedly showing permission requests after the user has declined.
- Use notification content that is appropriate for lock-screen visibility.
15. Creating an Android Notification Channel
Android uses notification channels to let users manage categories of notifications. An application might create different channels for order updates, messages, and reminders. Channel behavior is controlled by both application configuration and the user's system settings.
15.1 Create a Channel
async function configureAndroidNotificationChannel() {
if (Platform.OS !== "android") {
return;
}
await Notifications.setNotificationChannelAsync(
"general",
{
name: "General Notifications",
importance: Notifications.AndroidImportance.DEFAULT,
vibrationPattern: [0, 250, 250, 250]
}
);
}
Call the function during notification initialization where appropriate. Create channels before requesting permission when required by the targeted Android configuration. Choose importance and sound settings that match the notification's purpose, and avoid creating unnecessary duplicate channels.
16. Registering for a Push Notification Token
A push token identifies a destination for the selected push notification service. The application typically sends the token to its backend so the backend can direct notifications to the right app installation.
16.1 Obtain an Expo Push Token
async function registerForPushNotificationsAsync() {
await configureAndroidNotificationChannel();
const granted = await requestNotificationPermission();
if (!granted) {
return null;
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
throw new Error("Expo project ID is not configured.");
}
const tokenResult =
await Notifications.getExpoPushTokenAsync({ projectId });
return tokenResult.data;
}
The returned value is an Expo Push Token. It is used with Expo Push Service. If the application sends notifications directly through FCM or APNs, use the native device token and the corresponding platform-specific workflow instead.
16.2 Register the Token with the Backend
async function registerTokenWithBackend(token, accessToken) {
const response = await fetch(
"https://api.example.com/device-tokens",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`
},
body: JSON.stringify({
token,
platform: Platform.OS
})
}
);
if (!response.ok) {
throw new Error("Device token registration failed.");
}
return response.json();
}
The endpoint and authentication format are illustrative. The backend should validate requests, associate the token with the right user or installation, support token updates, and remove invalid or obsolete registrations when appropriate. Do not send push messages directly from the mobile client using server credentials.
17. Handling Notifications When the App Is in the Foreground
When a notification arrives while an app is open, the operating system and notification library may handle presentation differently from the background case. The application can configure a notification handler to determine how foreground notifications should be presented.
17.1 Configure the Foreground Handler
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowBanner: true,
shouldShowList: true,
shouldPlaySound: false,
shouldSetBadge: false
})
});
Configure the handler during application initialization, outside a component render cycle. Use the fields supported by the installed expo-notifications version. Presentation behavior may differ by platform and version.
17.2 Listen for Incoming Notifications
useEffect(() => {
const subscription =
Notifications.addNotificationReceivedListener(
(notification) => {
const data = notification.request.content.data;
console.log("Notification received:", data);
}
);
return () => subscription.remove();
}, []);
Use the listener to update the interface or trigger suitable refresh behavior. Avoid putting sensitive notification information into logs. Remove the listener when the component is unmounted so that subscriptions do not accumulate.
18. Handling Notification Taps
A notification can contain data identifying the screen or record that the user wants to open. The application should validate that data and navigate to the intended screen after navigation is ready.
18.1 Listen for Notification Responses
useEffect(() => {
const subscription =
Notifications.addNotificationResponseReceivedListener(
(response) => {
const data =
response.notification.request.content.data;
if (data?.type === "order" && data?.orderId) {
navigation.navigate("OrderDetails", {
orderId: data.orderId
});
}
}
);
return () => subscription.remove();
}, [navigation]);
This example assumes that navigation is available and ready and that the OrderDetails screen exists. A production app must handle cold starts, delayed navigation initialization, missing or invalid fields, expired sessions, and authorization checks.
18.2 Example Notification Payload
{
"to": "ExponentPushToken[example-token]",
"title": "Your Order Has Shipped",
"body": "Your order is on its way.",
"data": {
"type": "order",
"orderId": "ord_1001"
}
}
This is an illustrative payload for the Expo push service. The token, message, and order identifier must come from the actual application workflow. Notification data is not proof that the recipient is authorized to access the referenced resource; always verify access with the backend.
19. Sending Push Notifications from a Backend
Remote notifications should usually be sent by a backend service because the backend knows when relevant business events occur and can securely use the necessary credentials.
19.1 Example Server-Side Request to Expo Push Service
async function sendExpoPushNotification({
pushToken,
title,
body,
data
}) {
const response = await fetch(
"https://exp.host/--/api/v2/push/send",
{
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({
to: pushToken,
title,
body,
data
})
}
);
if (!response.ok) {
throw new Error("Unable to submit push notification.");
}
return response.json();
}
This example illustrates a server-side request. In production, call this from a backend environment, not from application code shipped to users. Add server-side validation, suitable credentials where required, delivery-ticket processing, throttling, retry policies, invalid-token cleanup, and monitoring. A successful HTTP response does not necessarily mean that the message was delivered or displayed.
19.2 Common Notification Errors
- Missing or incorrect push credentials.
- Invalid or outdated device tokens.
- Permission denied by the user.
- Missing Android notification channels or incorrect configuration.
- Expired authentication during token registration.
- Payloads that exceed service constraints.
- Network unavailability or push service failures.
- Notification delivery delayed by device settings or operating-system policies.
20. Local Notifications and Scheduled Reminders
Local notifications are useful when a reminder can be scheduled on the device without waiting for a server event. They can support study reminders, task deadlines, and scheduled prompts.
20.1 Schedule a Local Notification
async function scheduleReminder() {
await Notifications.scheduleNotificationAsync({
content: {
title: "Study Reminder",
body: "Continue your React Native practice.",
data: {
type: "study_reminder"
}
},
trigger: {
type:
Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL,
seconds: 60
}
});
}
This example schedules a reminder using a time interval. Actual presentation depends on notification permissions, platform behavior, and device settings. Calendar-based reminders and repeat schedules require the appropriate trigger configuration for the installed library version.
20.2 Cancel or Manage Scheduled Notifications
async function cancelScheduledReminders() {
await Notifications.cancelAllScheduledNotificationsAsync();
}
Cancel all scheduled notifications only when that is the intended action. For applications with multiple types of reminders, track individual notification identifiers so that the user can cancel one reminder without affecting the others.
21. Notification Security and User Experience
Notifications can appear on a lock screen or be visible to someone nearby. Their content should be designed with privacy and user choice in mind.
- Do not include passwords, access tokens, payment credentials, or unnecessary personal data.
- Use generic text for sensitive events where detailed text could disclose private information.
- Verify the user's authorization before displaying protected records opened from a notification.
- Allow users to control notification preferences wherever appropriate.
- Remove stale device tokens from the backend.
- Respect notification permissions and quiet hours where the application supports them.
- Use idempotent backend processing for important notification-triggering events to reduce accidental duplicates.
- Handle repeated taps safely and prevent the same action from being processed twice.
22. Introduction to React Native Performance Optimization
Performance optimization means identifying and reducing unnecessary work so the application starts quickly, responds to touch, scrolls smoothly, uses memory efficiently, and manages network and storage operations appropriately.
22.1 What Affects Performance?
- Repeated React component renders.
- Large or inefficient lists.
- Heavy images or excessive image decoding.
- Expensive calculations on the JavaScript thread.
- Too many API requests or repeated data processing.
- Large application bundles and unnecessary dependencies.
- Memory leaks and subscriptions that are not cleaned up.
- Animations or effects that perform excessive work.
- Running the application in development mode during performance evaluation.
22.2 Understanding Frame Rate
A smooth interface generally aims to produce frames at a consistent rate. At 60 frames per second, each frame has approximately 16.67 milliseconds available for processing. When important work exceeds the available time, the application may drop frames, making animation or scrolling look uneven.
React Native applications have JavaScript execution and native UI rendering responsibilities. Heavy JavaScript work may delay state updates and event processing, while native rendering work can affect visual presentation. Investigate the actual bottleneck before selecting an optimization.
23. Measure Performance Before Optimizing
Performance work should begin by identifying a reproducible problem. Optimizing code without measuring it can increase complexity without improving the user's experience.
23.1 Useful Performance Measurements
| Metric |
What It Indicates |
| Cold start time |
How long the application takes to launch from a stopped state. |
| Screen load time |
How long important content takes to become usable. |
| Frame rate and dropped frames |
Whether scrolling and animations appear smooth. |
| Component render activity |
Whether components render more often than needed. |
| Memory usage |
Whether the application consumes excessive memory or retains objects unnecessarily. |
| Network latency |
How long requests and responses take. |
| Cache hit rate |
How often data can be served from an existing cache instead of a new request. |
| Crash and error rate |
Whether changes improve stability. |
23.2 Development and Release Builds
Development builds include debugging features and additional checks that may reduce performance. Use development tools to find defects, but measure final responsiveness on realistic release builds and representative target devices.
23.3 Performance Tools
- React Native DevTools and the supported development profiling tools.
- React Profiler for render activity.
- Android Studio profiling tools for Android performance and memory investigation.
- Xcode Instruments for iOS CPU, memory, and rendering investigation.
- Network inspectors and backend monitoring for request latency.
- Application crash and performance monitoring tools configured with suitable privacy controls.
24. Reducing Unnecessary Re-Renders
A component re-renders when React determines it needs to reflect updated state, props, or context. Some re-renders are necessary, but repeated work across large component trees can become expensive.
24.1 Use State at the Appropriate Level
Keep temporary state close to the component that owns it when possible. If every small input updates a high-level component containing a large list, unrelated portions of the interface may re-render more frequently than needed.
24.2 Use React.memo
import React, { memo } from "react";
import { Text, View } from "react-native";
const ProductRow = memo(function ProductRow({ product }) {
return (
<View>
<Text>{product.name}</Text>
<Text>₹{product.price}</Text>
</View>
);
});
export default ProductRow;
React.memo can skip some renders when props are unchanged according to its comparison behavior. It is most useful when a component renders frequently with stable props. It is not a guarantee that a component will never re-render, and it is not automatically beneficial for every component.
24.3 Use useMemo for Expensive Calculations
const filteredProducts = useMemo(() => {
return products.filter((product) =>
product.name.toLowerCase().includes(
searchText.toLowerCase()
)
);
}, [products, searchText]);
useMemo can avoid repeating a calculation while its dependencies remain unchanged. For a tiny list, a direct calculation may be simpler and sufficiently fast. Measure before adding memoization.
24.4 Use useCallback When It Helps
const handleProductPress = useCallback((productId) => {
navigation.navigate("ProductDetails", { productId });
}, [navigation]);
useCallback can preserve a function reference between renders when its dependencies have not changed. It is particularly useful when passing callbacks to memoized child components or when a dependency-sensitive hook needs a stable reference.
25. Optimizing FlatList and Large Lists
FlatList is designed to render long collections efficiently by mounting only a subset of the rows at a time. It is generally preferable to a ScrollView containing every item when the list may become large.
25.1 Use Stable Keys
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<ProductRow product={item} />
)}
/>
Use a stable unique identifier rather than the item index when records can be added, removed, or reordered.
25.2 Optimize Fixed-Height Rows with getItemLayout
<FlatList
data={products}
keyExtractor={(item) => item.id}
getItemLayout={(data, index) => ({
length: 72,
offset: 72 * index,
index
})}
renderItem={({ item }) => (
<ProductRow product={item} />
)}
/>
This example assumes each row has a fixed height of 72 units and no additional row spacing that changes the item measurements. Correct measurements help FlatList skip some layout calculations. Do not use this exact layout function when rows have variable heights.
25.3 Additional List Optimization Techniques
- Keep each row component relatively lightweight.
- Avoid costly calculations inside renderItem.
- Use memoized rows when profiling shows repeated expensive rendering.
- Use appropriately sized thumbnail images.
- Use pagination or incremental loading for very large collections.
- Adjust windowSize, initialNumToRender, and maxToRenderPerBatch only after measuring the effect.
- Test both scrolling smoothness and responsiveness of buttons inside rows.
26. Optimizing Images and Media
Product images, thumbnails, banners, and profile images can use significant network bandwidth and memory. Large images that are displayed in small components waste resources and may slow down rendering.
26.1 Image Optimization Techniques
- Request image sizes close to the dimensions required by the interface.
- Use compressed image formats appropriate to the supported platforms.
- Use thumbnails in long lists and larger images on detail screens.
- Cache images according to a sensible expiration and invalidation policy.
- Display placeholders while remote images load.
- Provide fallback content when an image cannot load.
- Avoid decoding many large images at the same time.
- Use a maintained image library if advanced caching or image processing is required.
26.2 Example Image Loading States
const [imageLoaded, setImageLoaded] = useState(false);
const [imageFailed, setImageFailed] = useState(false);
{!imageLoaded && !imageFailed && (
<Text>Loading image...</Text>
)}
{imageFailed ? (
<Text>Image unavailable</Text>
) : (
<Image
source={{ uri: product.image }}
onLoad={() => setImageLoaded(true)}
onError={() => setImageFailed(true)}
/>
)}
This is an illustrative JSX fragment. In a real component, import Image from React Native and ensure the image has appropriate dimensions. Use a visually suitable placeholder component when an image is loading rather than relying solely on text.
27. Optimizing API Requests and Data Processing
Network operations can make a screen feel slow even when the rendering code is efficient. A good data strategy limits unnecessary requests and avoids expensive processing on the main interaction path.
27.1 Network Optimization Techniques
- Fetch only the data required by the current screen.
- Use pagination for large datasets.
- Cache reusable responses when the data's freshness requirements allow it.
- Deduplicate identical in-flight requests.
- Debounce rapidly changing search input.
- Use cancellation or stale-response protection when a screen changes before a request completes.
- Set sensible request timeouts and retry policies.
- Use structured error handling rather than repeating request logic in every screen.
27.2 Debounce Search Input
useEffect(() => {
const timer = setTimeout(() => {
loadProducts(searchText);
}, 300);
return () => clearTimeout(timer);
}, [searchText]);
This example delays the search request until the user has stopped typing for approximately 300 milliseconds. The loadProducts function should be stable or defined according to the application's data-fetching strategy. For production code, also handle request cancellation and out-of-order responses so that older results do not replace newer results.
28. Optimizing Animations and Interactions
Animations should provide feedback without delaying touch handling or blocking important work. The best animation technique depends on the animation type, supported library, and target platform.
28.1 Animation Best Practices
- Prefer supported native-driven animation paths when an animation can use them.
- Avoid expensive calculations during every animation frame.
- Keep complex screen transitions lightweight.
- Use small visual feedback interactions instead of overly complex effects.
- Test animations on lower-end target devices.
- Ensure animations do not prevent users from interacting with important controls.
- Respect reduced-motion preferences where supported by the design and platform.
28.2 Example Using the Animated API
import React, { useEffect, useRef } from "react";
import { Animated, Text } from "react-native";
export default function FadeInMessage() {
const opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
const animation = Animated.timing(opacity, {
toValue: 1,
duration: 300,
useNativeDriver: true
});
animation.start();
return () => animation.stop();
}, [opacity]);
return (
<Animated.View style={{ opacity }}>
<Text>Welcome back!</Text>
</Animated.View>
);
}
This example animates opacity through a native-driven animation. The native driver supports specific properties, such as opacity and transforms, rather than every style property. Follow the API documentation for the version used in the project.
29. Reducing Memory Usage and Preventing Leaks
Memory problems can cause poor responsiveness, operating-system termination, or unexpected crashes. Applications should release resources that are no longer required.
29.1 Common Causes of Memory Problems
- Event listeners are not removed.
- Timers continue running after a component unmounts.
- Large objects remain referenced unnecessarily.
- Too many list items or large images are retained in memory.
- Repeated navigation creates duplicate subscriptions.
- Global caches grow without limits.
29.2 Clean Up Event Subscriptions
useEffect(() => {
const subscription = Notifications
.addNotificationReceivedListener((notification) => {
// Handle the notification.
});
return () => {
subscription.remove();
};
}, []);
Apply the same cleanup principle to connectivity listeners, event emitters, timers, animation callbacks, and other subscriptions. Use each library's supported cleanup API.
29.3 Avoid Unnecessary State and Cache Growth
- Remove stale records when the application no longer needs them.
- Apply cache limits or expiration policies.
- Do not store duplicate copies of large datasets without a reason.
- Paginate long lists instead of loading everything at once.
- Do not keep sensitive or outdated data indefinitely.
- Test repeated navigation, backgrounding, and foregrounding for resource leaks.
30. Application Startup and Bundle Optimization
Startup performance affects the user's first impression. Large dependencies, unnecessary initialization, and heavy synchronous work can increase the time before the application becomes usable.
30.1 Startup Optimization Practices
- Load only the essential data needed for the first screen.
- Defer noncritical work until after the initial interface is usable.
- Avoid large synchronous computations during initial rendering.
- Remove unused dependencies and dead code where possible.
- Use platform-appropriate production builds.
- Review image assets and bundle size.
- Initialize notifications and storage carefully without blocking unrelated screens.
- Show useful loading feedback while essential data is being prepared.
30.2 Avoid Excessive Console Logging
Frequent console logging, especially in large loops or frequently rendered components, can create unnecessary overhead. Remove noisy production logs and prevent sensitive data from being logged. Keep appropriate structured error reporting for troubleshooting.
31. Integrating Offline Storage, Notifications, and Performance
These three areas work best when they are designed together. An offline-capable application may cache data locally, receive a push notification when something changes on the server, and refresh only the relevant content when the user returns to the app.
31.1 Example: Offline-Capable Shopping App
- Load previously saved product information from SQLite or another suitable local store.
- Display cached products immediately when available.
- Fetch current products from the backend when connectivity permits.
- Update the local database and refresh the displayed list after a successful response.
- Register the application installation for relevant notifications after permission and configuration requirements are satisfied.
- Receive an order-shipped notification and extract the order identifier.
- Open the appropriate order screen when the user taps the notification.
- Fetch the latest authorized order details from the backend, using cached information as a fallback where appropriate.
- Optimize the order and product lists so that they remain responsive with larger datasets.
- Measure startup time, screen load time, render activity, and request latency to identify remaining bottlenecks.
31.2 Architectural Recommendations
- Storage layer: Centralize persistence in services or repositories.
- Data layer: Separate API calls from screen components.
- Notification layer: Centralize registration, event listeners, and notification routing.
- State layer: Keep global state limited to information shared by multiple components.
- Performance layer: Profile slow screens and large lists before changing code.
- Security layer: Keep secrets on the backend and verify protected operations remotely.
32. Practical Project: Offline Notes App with Push Notifications
Build a small React Native application that allows users to create notes, access saved notes offline, receive a reminder notification, and navigate through the application efficiently.
32.1 Functional Requirements
- Create, view, edit, and delete notes.
- Save notes persistently on the device.
- Display saved notes when the device is offline.
- Show useful loading, empty, and error states.
- Schedule a local reminder notification.
- Optionally register for remote push notifications.
- Open the relevant note when a supported notification is tapped.
- Optimize the notes list using FlatList and efficient row components.
- Handle storage failures without crashing the application.
32.2 Suggested Project Structure
OfflineNotesApp/
├── src/
│ ├── components/
│ │ ├── NoteCard.js
│ │ └── LoadingState.js
│ ├── screens/
│ │ ├── NotesScreen.js
│ │ ├── NoteDetailsScreen.js
│ │ └── SettingsScreen.js
│ ├── services/
│ │ ├── storageService.js
│ │ ├── notificationService.js
│ │ └── syncService.js
│ ├── database/
│ │ └── notesDatabase.js
│ └── navigation/
│ └── AppNavigator.js
├── App.js
└── package.json
32.3 Development Phases
| Phase |
Tasks |
Expected Result |
| Phase 1 |
Create the project and configure navigation. |
A working app foundation. |
| Phase 2 |
Create note forms and a notes list. |
Users can manage notes. |
| Phase 3 |
Add AsyncStorage or SQLite persistence. |
Notes remain available after restart. |
| Phase 4 |
Add optional server synchronization. |
Local and remote data can be reconciled. |
| Phase 5 |
Configure local notifications. |
Users can schedule reminders. |
| Phase 6 |
Configure remote push notifications if required. |
The backend can deliver relevant messages. |
| Phase 7 |
Optimize list rendering and state updates. |
The interface responds efficiently. |
| Phase 8 |
Test offline behavior, permissions, and failures. |
The app is more reliable. |
33. Common Problems and Solutions
| Problem |
Possible Cause |
Solution |
| Saved data disappears |
Only in-memory state is used or persistence fails. |
Implement persistent storage and handle storage errors. |
| Stored JSON cannot be parsed |
Data is malformed or a schema has changed. |
Validate the data, provide defaults, and implement migrations where needed. |
| Private information is exposed |
Sensitive data is saved in unencrypted storage. |
Minimize local data and use an appropriate secure storage mechanism. |
| Offline edits never sync |
No queue or connectivity recovery process exists. |
Add sync state, retry policies, and observable failure handling. |
| Push token cannot be created |
Permission, project configuration, or credentials are missing. |
Verify setup, permission status, token configuration, and platform requirements. |
| Notifications do not appear |
Permission or channel settings prevent presentation. |
Check notification settings, channel configuration, credentials, and device behavior. |
| Notification opens the wrong screen |
Payload data is invalid or the routing logic is incorrect. |
Validate payload fields and handle navigation readiness. |
| List scrolling is slow |
Rows are expensive or too many elements render. |
Profile the list, use FlatList effectively, and optimize row components. |
| App startup is slow |
Heavy initial work or unnecessary dependencies. |
Defer noncritical tasks and measure release startup performance. |
| App consumes too much memory |
Large caches, images, or uncleared subscriptions. |
Limit retained data and clean up resources. |
34. Best Practices Checklist
Offline Storage
- Choose a storage method appropriate for the data.
- Serialize and validate data when using key-value storage.
- Keep sensitive credentials out of ordinary AsyncStorage.
- Prevent initial loading from overwriting cached data.
- Define expiration, deletion, and migration rules.
- Implement retry and conflict handling for offline changes.
Push Notifications
- Request permission with a clear explanation.
- Configure platform credentials and notification channels correctly.
- Register and update tokens with the backend.
- Send remote notifications from a trusted server.
- Handle foreground events and notification taps.
- Validate notification data before navigation.
- Respect privacy, user preferences, and operating-system rules.
Performance Optimization
- Measure real performance problems before optimizing.
- Test performance in release builds.
- Use FlatList for large collections.
- Keep list rows and image assets lightweight.
- Use memoization when it produces a measured benefit.
- Clean up listeners, timers, and other resources.
- Reduce unnecessary requests and repeated calculations.
- Monitor crashes, memory usage, and responsiveness.
35. Interview Questions and Answers
Q1. What is offline storage in React Native?
Offline storage saves data on the device so the application can access it across sessions or when network connectivity is unavailable.
Q2. What is AsyncStorage?
AsyncStorage is a persistent asynchronous key-value storage solution for React Native. It stores string values and is useful for non-sensitive data such as preferences and small caches.
Q3. When should SQLite be used instead of AsyncStorage?
SQLite is useful for structured datasets that need queries, filtering, relationships, or larger collections of local records. AsyncStorage is often simpler for small key-value data.
Q4. Is AsyncStorage suitable for passwords?
No. AsyncStorage is unencrypted and should not be treated as a secure credential store. Use an appropriate secure-storage solution for supported sensitive values and follow secure authentication practices.
Q5. What does offline-first mean?
Offline-first design treats local data as an important part of the application experience. It can show cached data immediately and synchronize changes or refresh content when the server becomes reachable.
Q6. What is offline synchronization?
Offline synchronization coordinates local and server changes. It typically involves a pending-change queue, retries, duplicate-request protection, and conflict-resolution rules.
Q7. What is the difference between local and push notifications?
Local notifications are scheduled by the application on the device. Push notifications are delivered from a remote system through a platform or notification service.
Q8. What is a push token?
A push token identifies a destination for a push notification service. The application registers it with the backend so relevant notifications can be sent to the appropriate installation.
Q9. What is Expo Push Service?
Expo Push Service is a service for sending push notifications to configured Expo applications. The backend submits notification messages using Expo push tokens and processes the service's responses.
Q10. Why are notification permissions required?
Operating systems control notification presentation through user permissions and platform settings. Permission behavior differs between operating systems and versions.
Q11. What is an Android notification channel?
An Android notification channel groups notifications into a category so users can control properties such as importance and sound.
Q12. What causes poor React Native performance?
Common causes include unnecessary re-renders, heavy list rows, large images, repeated network requests, expensive JavaScript work, memory leaks, and excessive initialization.
Q13. Why use FlatList?
FlatList renders a subset of list items at a time and is designed for large collections. Proper keys, lightweight row components, and suitable layout settings can improve its performance.
Q14. What does React.memo do?
React.memo can prevent some unnecessary component renders when props remain unchanged. It is most useful for components whose rendering cost and update patterns justify memoization.
Q15. What is the purpose of useMemo?
useMemo caches a computed result between renders while its dependencies remain unchanged. It can reduce repeated expensive calculations when used appropriately.
Q16. How can an application prevent memory leaks?
Clean up listeners, subscriptions, timers, and animations. Avoid retaining large objects unnecessarily and inspect memory usage during repeated navigation and screen updates.
Q17. Why should performance be tested in release builds?
Development builds include extra debugging work that can affect performance. Release builds provide a more realistic picture of how the final application behaves.
Q18. How should an offline application handle conflicting changes?
It should follow an explicit conflict-resolution policy, such as server-wins, version-aware merge, field-level merge, or user resolution, based on the data's requirements.
36. Practice Exercises
- Create a storage service that saves and retrieves application preferences.
- Persist a shopping cart or a list of notes using AsyncStorage.
- Add validation for malformed or outdated stored values.
- Create a SQLite table and implement insert, update, read, and delete operations.
- Store a suitable session token using a secure-storage library.
- Display cached content before refreshing it from an API.
- Show an offline status message when remote content cannot be fetched.
- Create a local reminder notification using expo-notifications.
- Request notification permission and obtain a push token.
- Register the token with a test backend.
- Build a backend endpoint that sends a push notification through the selected service.
- Handle a notification tap and navigate to the correct screen.
- Optimize a large list using FlatList and stable keys.
- Use React.memo or useMemo where profiling indicates a benefit.
- Test the application with slow connectivity, denied permissions, and repeated screen navigation.
37. Quick Revision Notes
- Offline storage keeps selected information available on the device.
- AsyncStorage is an asynchronous, persistent, unencrypted key-value store.
- SQLite is useful for structured, searchable local datasets.
- Secure storage is appropriate for certain sensitive credentials.
- Offline-first design can display local data before refreshing it from a server.
- Offline synchronization needs retry handling and conflict-resolution rules.
- Push notifications are delivered through a remote notification service.
- Notification setup includes permissions, credentials, tokens, and platform configuration.
- The backend should send remote notifications and protect its credentials.
- Notification taps should be validated and routed safely.
- Performance optimization starts with measurement and profiling.
- FlatList, efficient rows, appropriate image sizes, and controlled renders can improve responsiveness.
- Listeners and timers must be cleaned up when they are no longer needed.
- Test application performance in release builds on representative devices.
38. Official Learning Resources
39. Conclusion
Offline storage, push notifications, and performance optimization are important parts of building reliable React Native applications. Local storage allows suitable data to remain accessible between sessions, synchronization helps coordinate local and remote changes, and notifications provide a channel for timely communication with users.
Performance optimization ensures that these features do not make the application slow or difficult to use. By choosing appropriate storage methods, handling notification permissions and tokens correctly, using secure backend workflows, cleaning up resources, and measuring application behavior, developers can build responsive and dependable mobile experiences.
To explore more mobile application development topics, visit React Native Training at JustAcademy, register for a course demo, or browse the JustAcademy website.